package com.abso.mp3tunes.locker.ui.viewers;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.jface.action.GroupMarker;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DragSourceListener;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

import com.abso.mp3tunes.locker.core.data.Album;
import com.abso.mp3tunes.locker.core.data.Artist;
import com.abso.mp3tunes.locker.core.data.Playlist;
import com.abso.mp3tunes.locker.core.data.Track;
import com.abso.mp3tunes.locker.ui.actions.AddToNewPlaylistAction;
import com.abso.mp3tunes.locker.ui.actions.AppendToPlaylistAction;
import com.abso.mp3tunes.locker.ui.actions.DeletePlaylistsAction;
import com.abso.mp3tunes.locker.ui.actions.ExportASXPlaylistAction;
import com.abso.mp3tunes.locker.ui.actions.ExportM3UPlaylistAction;
import com.abso.mp3tunes.locker.ui.actions.ExportXSPFPlaylistAction;
import com.abso.mp3tunes.locker.ui.actions.OpenLastFMAlbumPageAction;
import com.abso.mp3tunes.locker.ui.actions.OpenLastFMArtistPageAction;
import com.abso.mp3tunes.locker.ui.actions.OpenLastFMTrackPageAction;
import com.abso.mp3tunes.locker.ui.actions.PlayAction;
import com.abso.mp3tunes.locker.ui.actions.RemoveFromPlaylistsAction;
import com.abso.mp3tunes.locker.ui.actions.RenamePlaylistAction;
import com.abso.mp3tunes.locker.ui.dnd.LockerTransfer;
import com.abso.mp3tunes.locker.ui.views.LockerBrowserView;

/**
 * An abstract base class for tree viewers showing Locker data objects.
 */
public abstract class LockerViewer extends TreeViewer {

	/**
	 * If <code>true</code> the viewer is in a dirty state, thus requiring
	 * refresh.
	 */
	private boolean dirty;

	/** The Locker browser. */
	private LockerBrowserView lockerBrowser;

	/** The play action. */
	private PlayAction playAction;

	/**
	 * Constructs a new viewer.
	 * 
	 * @param parent
	 *            the parent control.
	 * @param lockerBrowser
	 *            the Locker browser.
	 */
	public LockerViewer(Composite parent, LockerBrowserView lockerBrowser) {
		super(new Tree(parent, SWT.MULTI));
		this.lockerBrowser = lockerBrowser;
		setUseHashlookup(true);
		setContentProvider(new LockerViewerContentProvider());
		this.dirty = true;

		/* sets the popup menu manager */
		playAction = new PlayAction(this);
		final AddToNewPlaylistAction addToNewPlaylistAction = new AddToNewPlaylistAction(
				this);
		final AppendToPlaylistAction appendToPlaylistAction = new AppendToPlaylistAction(
				this, addToNewPlaylistAction);
		final DeletePlaylistsAction deletePlaylistsAction = new DeletePlaylistsAction(
				this);
		final RenamePlaylistAction renamePlaylistAction = new RenamePlaylistAction(
				this);
		final RemoveFromPlaylistsAction removeFromPlaylistsAction = new RemoveFromPlaylistsAction(
				this);
		final ExportASXPlaylistAction exportASXAction = new ExportASXPlaylistAction(
				this);
		final ExportM3UPlaylistAction exportM3UAction = new ExportM3UPlaylistAction(
				this);
		final ExportXSPFPlaylistAction exportXSPFAction = new ExportXSPFPlaylistAction(
				this);
		final OpenLastFMArtistPageAction openLastFMArtistPageAction = new OpenLastFMArtistPageAction(this);
		final OpenLastFMAlbumPageAction openLastFMAlbumPageAction = new OpenLastFMAlbumPageAction(this);
		final OpenLastFMTrackPageAction openLastFMTrackPageAction = new OpenLastFMTrackPageAction(this);
		MenuManager popupMenuManager = new MenuManager();
		IMenuListener listener = new IMenuListener() {
			public void menuAboutToShow(IMenuManager menuMgr) {
				menuMgr.removeAll();

				/* prepares the menu groups */
				menuMgr.add(new GroupMarker("play"));
				menuMgr.add(new GroupMarker("playlist"));
				menuMgr.add(new GroupMarker("export"));
				menuMgr.add(new GroupMarker("open"));
				menuMgr.appendToGroup("open", new Separator());
				menuMgr.appendToGroup("playlist", new Separator());
				IMenuManager exportSubMenuMgr = new MenuManager("Export Playlist", "exportPlaylist");
				menuMgr.appendToGroup("export", new Separator());
				menuMgr.appendToGroup("export", exportSubMenuMgr);

				/* adds the actions */
				List selectedItems = getSelectedItems();
				if (!selectedItems.isEmpty()) {
					playAction.setSelectedElements(selectedItems);
					menuMgr.appendToGroup("play", playAction);
					if (!isPlaylistOnly(selectedItems)) {
						addToNewPlaylistAction
								.setSelectedElements(selectedItems);
						appendToPlaylistAction
								.setSelectedElements(selectedItems);
						exportASXAction.setSelectedElements(selectedItems);
						exportM3UAction.setSelectedElements(selectedItems);
						exportXSPFAction.setSelectedElements(selectedItems);
						menuMgr.appendToGroup("playlist",
								addToNewPlaylistAction);
						menuMgr.appendToGroup("playlist",
								appendToPlaylistAction);
						exportSubMenuMgr.add(exportASXAction);
						exportSubMenuMgr.add(exportM3UAction);
						exportSubMenuMgr.add(exportXSPFAction);
					}
				}
				List playlistsAndTracks = getSelectedPlaylistsTracks();
				if (!playlistsAndTracks.isEmpty()) {
					removeFromPlaylistsAction
							.setPlaylistsAndTracks(playlistsAndTracks);
					menuMgr
							.appendToGroup("playlist",
									removeFromPlaylistsAction);
				}
				List selectedPlaylists = getSelectedPlaylistsToEdit();
				if (!selectedPlaylists.isEmpty()) {
					deletePlaylistsAction.setPlaylists(selectedPlaylists);
					menuMgr.appendToGroup("playlist", deletePlaylistsAction);
					if (selectedPlaylists.size() == 1) {
						renamePlaylistAction
								.setPlaylist((Playlist) selectedPlaylists
										.get(0));
						menuMgr.appendToGroup("playlist", renamePlaylistAction);
					}
				}
				if (selectedItems.size() == 1) {
					Object item = selectedItems.get(0);
					if (item  instanceof Artist) {
						openLastFMArtistPageAction.setSelectedArtist((Artist) item);
						menuMgr.appendToGroup("open", openLastFMArtistPageAction);
					} else if (item instanceof Album) {
						openLastFMAlbumPageAction.setSelectedAlbum((Album) item);
						menuMgr.appendToGroup("open", openLastFMAlbumPageAction);
					} else if (item instanceof Track) {
						openLastFMTrackPageAction.setSelectedTrack((Track) item);
						menuMgr.appendToGroup("open", openLastFMTrackPageAction);
					}
				}
			}

		};
		popupMenuManager.addMenuListener(listener);
		popupMenuManager.setRemoveAllWhenShown(true);
		Menu menu = popupMenuManager.createContextMenu(getTree());
		getTree().setMenu(menu);

		/* adds the key listener */
		getTree().addKeyListener(new KeyAdapter() {
			public void keyReleased(KeyEvent e) {
				if ((e.character == SWT.DEL) && (e.stateMask == 0)) {
					List selectedPlaylists = getSelectedPlaylistsToEdit();
					if (!selectedPlaylists.isEmpty()) {
						deletePlaylistsAction.setPlaylists(selectedPlaylists);
						deletePlaylistsAction.run();
					} else {
						List playlistsAndTracks = getSelectedPlaylistsTracks();
						if (!playlistsAndTracks.isEmpty()) {
							removeFromPlaylistsAction
									.setPlaylistsAndTracks(playlistsAndTracks);
							removeFromPlaylistsAction.run();
						}
					}
				}
			}
		});

		/* adds the double click listener */
		addDoubleClickListener(new IDoubleClickListener() {
			public void doubleClick(DoubleClickEvent event) {
				IStructuredSelection selection = (IStructuredSelection) event
						.getSelection();
				Object element = selection.getFirstElement();
				if (selection.isEmpty()) {
					return;
				}
				boolean allTracks = true;
				for (Iterator i = selection.iterator(); i.hasNext();) {
					Object elem = i.next();
					if (!(elem instanceof Track)) {
						allTracks = false;
						break;
					}
				}
				if (allTracks) {
					playSelection();
				} else {
					if (selection instanceof ITreeSelection) {
						TreePath[] paths = ((ITreeSelection) selection)
								.getPathsFor(element);
						for (int i = 0; i < paths.length; i++) {
							setExpandedState(paths[i],
									!getExpandedState(paths[i]));
						}
					} else {
						setExpandedState(element, !getExpandedState(element));
					}
				}
			}
		});

		/* adds the drag listener */
		int ops = DND.DROP_COPY | DND.DROP_MOVE;
		Transfer[] transfers = new Transfer[] { LockerTransfer.INSTANCE };
		DragSourceListener dragSourceListener = new DragSourceAdapter() {
			public void dragSetData(DragSourceEvent event) {
				event.data = LockerViewer.this;
			}

			public void dragStart(DragSourceEvent event) {
				LockerTransfer.INSTANCE.setObject(LockerViewer.this);
			}

			public void dragFinished(DragSourceEvent event) {
				LockerTransfer.INSTANCE.setObject(null);
			}
		};
		addDragSupport(ops, transfers, dragSourceListener);
	}

	/**
	 * Gets whether this viewer content is out-of-date and needs to be
	 * refreshed.
	 * 
	 * @return <code>true</code> if the content must be refreshed.
	 */
	public final boolean isDirty() {
		return dirty;
	}

	/** Sets the viewer as dirty. */
	public final void setDirty() {
		this.dirty = true;
		((LockerViewerContentProvider) getContentProvider())
				.clearAllCachedChildren();
	}

	/** Updates the content of this viewer. */
	public final void updateContent() {
		doUpdateContent();
		this.dirty = false;
	}

	/** Performs the actual update of the viewer's content. */
	protected abstract void doUpdateContent();

	/**
	 * Returns the list of selected items.
	 * 
	 * @return the list of selected items.
	 */
	public List getSelectedItems() {
		ISelection sel = getSelection();
		if (!(sel instanceof IStructuredSelection)) {
			return Collections.EMPTY_LIST;
		}
		List elements = ((IStructuredSelection) sel).toList();
		List tracks = new ArrayList(elements.size());
		for (Iterator i = elements.iterator(); i.hasNext();) {
			Object elem = i.next();
			if (elem instanceof Track) {
				tracks.add(elem);
			} else if (elem instanceof Album) {
				tracks.add(elem);
			} else if (elem instanceof Artist) {
				tracks.add(elem);
			} else if (elem instanceof Playlist) {
				tracks.add(elem);
			} else {
				return Collections.EMPTY_LIST;
			}
		}
		return tracks;
	}

	/**
	 * Returns the set of selected playlists. Returns an empty list if the
	 * current selection includes at least an element which is not a playlist.
	 * 
	 * @return the set of selected playlists.
	 */
	private List getSelectedPlaylistsToEdit() {
		ISelection sel = getSelection();
		if (!(sel instanceof IStructuredSelection)) {
			return Collections.EMPTY_LIST;
		}
		List elements = ((IStructuredSelection) sel).toList();
		List playlists = new ArrayList(elements.size());
		for (Iterator i = elements.iterator(); i.hasNext();) {
			Object elem = i.next();
			if (elem instanceof Playlist) {
				Playlist playlist = (Playlist) elem;
				if (playlist.isReadOnly()) {
					return Collections.EMPTY_LIST;
				} else {
					playlists.add(playlist);
				}
			} else {
				return Collections.EMPTY_LIST;
			}
		}
		return playlists;
	}

	/**
	 * Returns the list of the selected tracks children of playlists. Returns an
	 * empty list if at least one of the selected item is not a track child of a
	 * playlist. An empty list is also returned if tracks of predefined playlist
	 * (Newest, Random, Recent) are selected.
	 * 
	 * @return the list of selected tracks children of playlists.
	 */
	private List getSelectedPlaylistsTracks() {
		Item[] items = getSelection(getTree());
		List playlistsAndTracks = new ArrayList();
		for (int i = 0; i < items.length; i++) {
			TreeItem treeItem = (TreeItem) items[i];
			if ((treeItem != null) && (treeItem.getData() instanceof Track)) {
				TreeItem parentTreeItem = treeItem.getParentItem();
				if ((parentTreeItem != null)
						&& (parentTreeItem.getData() instanceof Playlist)) {
					Playlist playlist = (Playlist) parentTreeItem.getData();
					if (!playlist.isReadOnly()) {
						playlistsAndTracks.add(new Object[] {
								parentTreeItem.getData(), treeItem.getData() });
					} else {
						return Collections.EMPTY_LIST;
					}
				} else {
					return Collections.EMPTY_LIST;
				}
			} else {
				return Collections.EMPTY_LIST;
			}
		}
		return playlistsAndTracks;
	}

	/**
	 * Checks whether all the specified items are playlists.
	 * 
	 * @param items
	 *            the items to check.
	 * @return <code>true</code> if <i>items</i> includes playlists only.
	 */
	private boolean isPlaylistOnly(List items) {
		for (Iterator i = items.iterator(); i.hasNext();) {
			if (!(i.next() instanceof Playlist)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Returns the Locker browser.
	 * 
	 * @return the Locker browser.
	 */
	public LockerBrowserView getLockerBrowser() {
		return lockerBrowser;
	}

	/**
	 * Runs the play action over the selected items.
	 */
	public void playSelection() {
		List selectedTracks = getSelectedItems();
		if (!getSelectedItems().isEmpty()) {
			playAction.setSelectedElements(selectedTracks);
			playAction.run();
		}
	}

}
